
//
// 110917 - AH - (Midtun) Created.
// 110919 - AH - Finished first version with all java doc, meaningful names and
//               possibility to both find product and factors.
// 120106 - AH - (Chaos) matchFactor(Input).
// 120125 - AH - (Sletten *) hasRowFactor(Factor) and hasRowFactor(Factor).
// 120125 - AH - (Huset) Renamed name property and added toString().
// 120127 - AH - Made iterable over products, removed m_products (duplicate
//               of m_cells.values()) and added some missing java doc.
// 120128a- AH - (Garage) changeRowFactor(SoekratName) and 
//               changeColFactor(SoekratName). 
// 120128b- AH - (Garage) getProduct(Factor, Factor).
// 120131a- AH - (Huset *) Fixed that renaming of factors with no product did
//               not work.
// 120131b- AH - (Huset *) isEmpty().
// 120321 - AH - Removed @Override for interface method (1.5 compatible).
//

package org.soekrat.engine.ontology.ptable;

import java.io.CharArrayWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.soekrat.engine.SoekratName;
import org.soekrat.engine.SoekratNameMatcher;
import org.soekrat.engine.SoekratNameMatcherSingleton;
import org.soekrat.engine.ontology.OntologyDatastructure;
import org.soekrat.engine.visitor.TokenizedLine;

/**
 * <p>
 *   {@link OntologyDatastructure} that is a product table.
 * </p>
 * @author Arne Halvorsen (AH)
 * @author Laetitia Le Chatton (LLC) 
 */
public final class ProductTable extends OntologyDatastructure 
  implements Iterable<Product>
{
  private final Map<String, Product> m_cells=new HashMap<String, Product>();
  
  private final List<ProductTableFactor> m_rowFactors=
    new ArrayList<ProductTableFactor>();
  
  private final List<ProductTableFactor> m_colFactors=
    new ArrayList<ProductTableFactor>();
  
  /**
   * <p>
   *   Constructor.
   * </p>
   */
  public ProductTable(){}
  
  /**
   * <p>
   *   Tells if is empty (contains now row or column factors 
   *   (and so, no products)).
   * </p>
   * @return {@code true} if is empty, {@code false} if is not.
   */
  public boolean isEmpty(){ return m_cells.isEmpty(); }
  
  /**
   * <p>
   *   Gets number of row factors.
   * </p>
   * @return Count.
   */
  public int getRowFactorCount(){ return m_rowFactors.size(); }
  
  /**
   * <p>
   *   Gets row factor.
   * </p>
   * @param idx Index of factor.
   * @return {@link ProductTableFactor}.
   * @throws IndexOutOfBoundsException If 
   *         {@code idx<0 || idx>=getRowFactorCount()}.
   */
  public ProductTableFactor getRowFactor(int idx)
  { 
    return m_rowFactors.get(idx); 
  }
  
  /**
   * <p>
   *   Gets number of column factors.
   * </p>
   * @return Count.
   */
  public int getColFactorCount(){ return m_colFactors.size(); }
  
  /**
   * <p>
   *   Gets column factor.
   * </p>
   * @param idx Index of factor.
   * @return {@link ProductTableFactor}.
   * @throws IndexOutOfBoundsException If 
   *         {@code idx<0 || idx>=getColFactorCount()}.
   */
  public ProductTableFactor getColFactor(int idx)
  { 
    return m_colFactors.get(idx); 
  }
    
  /**
   * <p>
   *   Gets
   *   {@link Product} given indexes of it's factor.
   * </p>
   * @param row Row factor index.
   * @param col Column factor index.
   * @return {@link Product}.
   * @throws IndexOutOfBoundsException If {@code row} or {@code col} is out of
   *         bounds.
   */
  public Product getProduct(int row, int col)
  {
    ProductTableFactor rf=m_rowFactors.get(row);
    ProductTableFactor cf=m_colFactors.get(col);
    String key=getKey(rf, cf);
    return m_cells.get(key);
  }
  
  @Override
  public void populate(TokenizedLine input) 
  {
    throw new IllegalStateException("Dont know yet");
  }
  
  /**
   * <p>
   *   Creates a 
   *   {@link Product} in {@code this} table.
   * </p>
   * @param fr Name on row factor.
   * @param fc Name on col factor.
   * @param p  Name on product f0xf1.
   * @return {@link Product}.
   * @throws IllegalArgumentException If product already exists. 
   */
  public Product create(String fr, String fc, String p)
  {
    if (fr==null)
    {
      throw new NullPointerException("f0");
    }
    if (fc==null)
    {
      throw new NullPointerException("f1");
    }
    if (p==null)
    {
      throw new NullPointerException("p");
    }
    
    SoekratName row=new SoekratName(fr);
    SoekratName col=new SoekratName(fc);
    SoekratName pname=new SoekratName(p);
    return create(row, col, pname);
  }
  
  /**
   * <p>
   *   Creates a 
   *   {@link Product} in {@code this} table.
   * </p>
   * @param frname Name on row factor.
   * @param fcname Name on column factor.
   * @param p  Name on product f0xf1.
   * @return {@link Product}.
   * @throws IllegalArgumentException If product already exists.
   */
  public Product create(SoekratName frname, SoekratName fcname, SoekratName p)
  {
    if (frname==null)
    {
      throw new NullPointerException("f0");
    }
    if (fcname==null)
    {
      throw new NullPointerException("f1");
    }
    if (p==null)
    {
      throw new NullPointerException("p");
    }
    
    ProductTableFactor fr=new ProductTableFactor(frname);
    ProductTableFactor fc=new ProductTableFactor(fcname);
    
    String key=getKey(fr, fc);
    if (m_cells.containsKey(key))
    {
      throw new IllegalArgumentException("cell exists");
    }
    
    Product retVal=new Product(frname, fcname, p);
    
    m_cells.put(key, retVal);
    
    addFactor(fr, m_rowFactors);
    addFactor(fc, m_colFactors);
    
    return retVal;
  }
  
  /**
   * <p>
   *   Tells if has row factor.
   * </p>
   * @param f {@link ProductTableFactor} to see if is row factor.
   * @return {@code true} if is, {@code false} if is not.
   */
  public boolean hasRowFactor(ProductTableFactor f)
  { 
    return hasFactor(f, m_rowFactors); 
  }
  
  /**
   * <p>
   *   Tells if has column factor.
   * </p>
   * @param f {@link ProductTableFactor} to see if is column factor.
   * @return {@code true} if is, {@code false} if is not.
   */
  public boolean hasColFactor(ProductTableFactor f)
  { 
    return hasFactor(f, m_colFactors); 
  }
  
  private static boolean hasFactor(ProductTableFactor f, 
    List<ProductTableFactor> fl)
  {
    if (f==null)
    {
      throw new NullPointerException("f");
    }
    
    for (ProductTableFactor factor : fl) if (f.equals(factor)) return true;
    return false;
  }
  
  /**
   * <p>
   *   Creates a 
   *   {@link Product} in {@code this} table for existing factors which do not
   *   have a
   *   {@code Product}.
   * </p>
   * @param row Row ProductTableFactor.
   * @param col Column ProductTableFactor.
   * @param p  Name on product.
   * @return {@link Product}.
   * @throws IllegalArgumentException If product already exists.
   */
  public Product create(ProductTableFactor row, ProductTableFactor col, 
    SoekratName p)
  {
    if (row==null)
    {
      throw new NullPointerException("row");
    }
    if (col==null)
    {
      throw new NullPointerException("col");
    }
    if (p==null)
    {
      throw new NullPointerException("p");
    }
    
    if (!hasRowFactor(row))
    {
      throw new IllegalArgumentException("row factor "+row+" does not exists");
    }
    if (!hasColFactor(col))
    {
      throw new IllegalArgumentException("row factor "+col+" does not exists");
    }
    
    String key=getKey(row, col);
    if (m_cells.containsKey(key))
    {
      throw new IllegalArgumentException("cell exists");
    }
    
    Product retVal=new Product(row.getSoekratName(), col.getSoekratName(), p);
    
    m_cells.put(key, retVal);
    
    return retVal;
  }
  
  /**
   * <p>
   *   Creates row factor.
   * </p>
   * @param name Factor name.
   * @return {@code true} if created, {@code false} if factor existed.
   */
  public boolean createRowFactor(String name)
  {
    if (name==null)
    {
      throw new NullPointerException("name");
    }
    
    ProductTableFactor f=new ProductTableFactor(new SoekratName(name));
    return addFactor(f, m_rowFactors);
  }
  
  /**
   * <p>
   *   Creates row factor.
   * </p>
   * @param name Factor name.
   * @return {@code true} if created, {@code false} if factor existed.
   */
  public boolean createColFactor(String name)
  {
    if (name==null)
    {
      throw new NullPointerException("name");
    }
    
    ProductTableFactor f=new ProductTableFactor(new SoekratName(name));
    return addFactor(f, m_colFactors);
  }
  
  private static boolean addFactor(ProductTableFactor f, 
    List<ProductTableFactor> factors)
  {
    if (!hasFactor(f, factors)) 
    {
      factors.add(f);
      return true;
    }
    return false;
  }
  
  /**
   * <p>
   *   Gets
   *   {@link Product}.
   * </p>
   * @param row Row factor.
   * @param col Column factor.
   * @return {@link Product} or {@code null} if not found.
   */
  public Product getProduct(ProductTableFactor row, ProductTableFactor col)
  {
    if (row==null)
    {
      throw new NullPointerException("name0");
    }
    if (col==null)
    {
      throw new NullPointerException("name1");
    }
    
    return getProduct(row.getSoekratName(), col.getSoekratName());
  }
  
  /**
   * <p>
   *   Gets
   *   {@link Product}.
   * </p>
   * @param row Name on row factor.
   * @param col Name on column factor.
   * @return {@link Product} or {@code null} if not found.
   */
  public Product getProduct(String row, String col)
  {
    if (row==null)
    {
      throw new NullPointerException("name0");
    }
    if (col==null)
    {
      throw new NullPointerException("name1");
    }
    
    SoekratName name0s=new SoekratName(row);
    SoekratName name1s=new SoekratName(col);
    
    return getProduct(name0s, name1s);
  }
  
  /**
   * <p>
   *   Gets
   *   {@link Product}.
   * </p>
   * @param fr Name on row factor.
   * @param f1 Name on col factor.
   * @return {@link Product} or {@code null} if not found.
   */
  public Product getProduct(SoekratName fr, SoekratName f1)
  {
    if (fr==null)
    {
      throw new NullPointerException("name0");
    }
    if (f1==null)
    {
      throw new NullPointerException("name1");
    }
    
    String key=getKey(fr, f1);
    
    Product retVal=m_cells.get(key);
    
    if (retVal==null)
    {
      key=getKey(f1, fr);
      retVal=m_cells.get(key);
    }
    
    return retVal;
  }
  
  /**
   * <p>
   *   Gets
   *   {@link Product}.
   * </p>
   * @param p Name on product.
   * @return {@link Product} or {@code null} if not found.
   */
  public List<Product> getProduct(SoekratName p)
  {
    if (p==null)
    {
      throw new NullPointerException("p");
    }
    
    return getProduct(p.getPure());
  }
  
  /**
   * <p>
   *   Gets the row factors that matches given string.
   * </p>
   * @param s String to match-
   * @return Found.
   */
  public List<ProductTableFactor> getRowFactors(String s)
  {
    if (s==null)
    {
      throw new NullPointerException("s");
    }
    
    return getFactors(s, m_rowFactors);
  }
  
  /**
   * <p>
   *   Gets the column factors that matches given string.
   * </p>
   * @param s String to match-
   * @return Found.
   */
  public List<ProductTableFactor> getColFactors(String s)
  {
    if (s==null)
    {
      throw new NullPointerException("s");
    }
    
    return getFactors(s, m_colFactors);
  }
  
  /**
   * <p>
   *   Matches
   *   {@link TokenizedLine}.
   * </p>
   * @param input {@link TokenizedLine} to match.
   * @return Result.
   */
  public FactorMatchResult matchFactor(TokenizedLine input)
  {
    if (input==null)
    {
      throw new NullPointerException("input");  
    }
    
    FactorMatchResult retVal=new FactorMatchResult();
    
    String[] ss=input.getUniqueStrings();
    for (String curr : ss) 
    {
      List<ProductTableFactor> f=getRowFactors(curr);
      retVal.add(f, true);  
      f=getColFactors(curr);
      retVal.add(f, false);
    }
    
    return retVal;
  }
  
  private static List<ProductTableFactor> getFactors(String s, 
    List<ProductTableFactor> f)
  {
    List<ProductTableFactor> retVal=new ArrayList<ProductTableFactor>();
    
    for (ProductTableFactor curr : f)
    {
      if (SoekratNameMatcherSingleton.INSTANCE.match(s, curr.getSoekratName())) 
      {
        retVal.add(curr);
      }
    }
    return retVal;
  }
  
  /**
   * <p>
   *   Gets
   *   {@link Product}.
   * </p>
   * @param p Name on product.
   * @return {@link Product} or {@code null} if not found.
   */
  public List<Product> getProduct(String p)
  {
    if (p==null)
    {
      throw new NullPointerException("p");
    }
    
    SoekratNameMatcher matcher=SoekratNameMatcherSingleton.INSTANCE;
    
    List<Product> retVal=new ArrayList<Product>();
    
    for (Product product : m_cells.values())
    {
      SoekratName name=product.getSoekratName();
      
      if (matcher.match(p, name))
      {
        retVal.add(product);
      }
    }
    
    return retVal;
  }
  
  /**
   * <p>
   *   Gets key a
   *   {@link Product} is indexed at.
   * </p>
   * @param f0 Product's first factor. 
   * @param f1 Product's second factor.
   * @return Key.
   */
  static String getKey(SoekratName f0, SoekratName f1)
  {
    StringBuilder sb=new StringBuilder();
    String n0=f1.getPure();
    String n1=f0.getPure();
    return sb.append(n0).append('_').append(n1).toString();
  }
  
  /**
   * <p>
   *   Removes row factors-
   * </p>
   * @param f Row factor to remove.
   */
  public void removeRowFactor(ProductTableFactor f){ removeFactor(f, true); }
  
  /**
   * <p>
   *   Removes Column factors-
   * </p>
   * @param f column factor to remove.
   */
  public void removeColFactor(ProductTableFactor f){ removeFactor(f, false); }
  
  /**
   * <p>
   *   Change name on a row factor.
   * </p>
   * @param oldf Old name.
   * @param newf New name.
   */
  public void changeRowFactor(SoekratName oldf, SoekratName newf)
  {
    changeFactor(oldf, newf, true);
  }
  
  /**
   * <p>
   *   Change name on a column factor.
   * </p>
   * @param oldf Old name.
   * @param newf New name.
   */
  public void changeColFactor(SoekratName oldf, SoekratName newf)
  {
    changeFactor(oldf, newf, false);
  }
  
  private void changeFactor(SoekratName oldf, SoekratName newf, boolean row)
  {
    if (oldf==null)
    {
      throw new NullPointerException("oldf");
    }
    if (newf==null)
    {
      throw new NullPointerException("newf");
    }
    
    if (row)
    {
      int idx=getRowFactorIndex(oldf);
      
      if (idx==-1) return;
      
      m_rowFactors.remove(idx);
      m_rowFactors.add(idx, new ProductTableFactor(newf));
    }
    else
    {
      int idx=getColFactorIndex(oldf);
      
      if (idx==-1) return;
      
      m_colFactors.remove(idx);
      m_colFactors.add(idx, new ProductTableFactor(newf));
    }
    
    List<Product> affected=new ArrayList<Product>();
    for (Product product : m_cells.values())
    {
      SoekratName factor=row ? product.getRowFactor() : product.getColFactor();
      
      if (factor.equals(oldf)) affected.add(product);
    }
    
    if (affected.isEmpty()) return;
    
    List<SoekratName> fs=new ArrayList<SoekratName>();
    List<SoekratName> pnames=new ArrayList<SoekratName>();
    
    for (Product product : affected) 
    {
      if (row) fs.add(product.getColFactor());
      else     fs.add(product.getRowFactor());
      
      pnames.add(product.getSoekratName());
      String key=product.getProductKey();
      m_cells.remove(key);
    }
    
    int n=fs.size();
    for (int i=0; i<n; i++)
    {
      SoekratName f=fs.get(i);
      SoekratName pname=pnames.get(i);
      Product np=row ? new Product(newf, f, pname) : 
                       new Product(f, newf, pname);
      m_cells.put(np.getProductKey(), np);
    }
  }
  
  public int getRowFactorIndex(SoekratName name)
  {
    if (name==null)
    {
      throw new NullPointerException("name");
    }
    
    int n=m_rowFactors.size();
    for (int i=0; i<n; i++) 
      if (m_rowFactors.get(i).getSoekratName().equals(name)) return i;
    return -1;
  }
  
  public int getColFactorIndex(SoekratName name)
  {
    if (name==null)
    {
      throw new NullPointerException("name");
    }
    
    int n=m_colFactors.size();
    for (int i=0; i<n; i++) 
      if (m_colFactors.get(i).getSoekratName().equals(name)) return i;
    return -1;
  }
  
  private void removeFactor(ProductTableFactor f, boolean row)
  {
    if (row) m_rowFactors.remove(f);
    else     m_colFactors.remove(f);
    
    Set<String> keys=new HashSet<String>();
    for (Map.Entry<String, Product> curr : m_cells.entrySet())
    {
      String key=curr.getKey();
      Product p=curr.getValue();
      boolean remove=false;
      if (row) remove=p.getRowFactor().equals(f);
      else     remove=p.getColFactor().equals(f);
      if (remove) keys.add(key);
    }
    
    for (String key : keys) m_cells.remove(key);
  }
  
  /**
   * <p>
   *   Gets key a
   *   {@link Product} is indexed at.
   * </p>
   * @param f0 Product's first factor. 
   * @param f1 Product's second factor.
   * @return Key.
   */
  static String getKey(ProductTableFactor f0, ProductTableFactor f1)
  {
    StringBuilder sb=new StringBuilder();
    String n0=f1.getSoekratName().getPure();
    String n1=f0.getSoekratName().getPure();
    return sb.append(n0).append('_').append(n1).toString();
  }
  
  @Override
  public String toString() 
  {
    CharArrayWriter caw=new CharArrayWriter();
    PrintWriter pw=new PrintWriter(caw);
    
    for (Product p : m_cells.values()) 
    {
      pw.println(p);
    }
    
    pw.flush();
    return caw.toString();
  }

  // Declared in Iterable<Product>.
  public Iterator<Product> iterator(){ return m_cells.values().iterator(); }
  
  // Test area
  
  private static void doMatchAndPrint(TokenizedLine input, ProductTable pt)
  {
    System.out.println("input: "+input);
    
    FactorMatchResult fm=pt.matchFactor(input);
    
    System.out.println("Both? "+fm.both());
    
    List<FactorPair> l=fm.getPairs();
    System.out.println("pairs:");
    System.out.println(FactorPair.toString(l));
  }
  
  public static void main(String[] args)
  {
    String p0="Soekrat";
    String p1="Charlotte";
    
    String fr0="Laetitia";
    String fc1="Arne";
    String fc2="Rat";
    
    
    ProductTable t=new ProductTable();
    
    t.create(fr0, fc1, p0);
    t.create(fr0, fc2, p1);
    
    TokenizedLine input=new TokenizedLine("Arne", "Laetitia", "x", "y", "Soekrat", "Rat");
    doMatchAndPrint(input, t);
    
    input=new TokenizedLine("Arne", "x", "y", "Soekrat", "Rat");
    doMatchAndPrint(input, t);
    
    input=new TokenizedLine("Laetitia", "x", "y", "Charlotte");
    doMatchAndPrint(input, t);
  }
  
  /**
   * <p>
   *   Simple demo/test program.
   * </p>
   * @param args Command line arguments, not used.
   */
  /*
  public static void main(String[] args) 
  {
    String p="Makes sense";
    String f0="Arne";
    String f1="Beer";
    String f2="Laetitia";
    String f3="Wine";
    
    ProductTable t=new ProductTable(new SoekratName("test"));
    
    t.create(f0, f1, p);
    t.create(f2, f3, p);
    
    System.out.println("looking up by factors:");
    System.out.println("  '"+f0+"'x'"+f1+"'='"+t.getProduct(f0, f1)+"'");
    System.out.println("  '"+f1+"'x'"+f0+"'='"+t.getProduct(f1, f0)+"'");
    System.out.println("  '"+f2+"'x'"+f3+"'='"+t.getProduct(f2, f3)+"'");
    System.out.println("  '"+f0+"'x'"+f2+"'='"+t.getProduct(f0, f2)+"'");
    System.out.println("  '"+f1+"'x'"+f3+"'='"+t.getProduct(f1, f3)+"'");
    
    System.out.println();
    
    System.out.println("finding factors of product '"+p+"'");
    List<Product> products=t.getProduct(p);
    for (Product product : products) 
    {
      System.out.println("  "+product);
    }
  }
  */
  
}
